Implement a wait.wait_exponential_jitter per Google's storage guide (#351)

* Implement a wait.wait_exponential_jitter per Google's storage retry guide

* Define a ClientError so Sphinx does not fail

* Fix spelling typos

* Simplify typing, replacing `int | float` with `float`

* Drop needless `#noqa`

Co-authored-by: Isaac Good <goodi@twosigma.com>
diff --git a/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml b/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml
new file mode 100644
index 0000000..870380c
--- /dev/null
+++ b/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Implement a wait.wait_exponential_jitter per Google's storage retry guide.
+    See https://cloud.google.com/storage/docs/retry-strategy
diff --git a/tenacity/__init__.py b/tenacity/__init__.py
index 1e3ff86..fd40376 100644
--- a/tenacity/__init__.py
+++ b/tenacity/__init__.py
@@ -63,6 +63,7 @@
 from .wait import wait_random  # noqa
 from .wait import wait_random_exponential  # noqa
 from .wait import wait_random_exponential as wait_full_jitter  # noqa
+from .wait import wait_exponential_jitter  # noqa
 
 # Import all built-in before strategies for easier usage.
 from .before import before_log  # noqa
diff --git a/tenacity/wait.py b/tenacity/wait.py
index aacb58d..289705c 100644
--- a/tenacity/wait.py
+++ b/tenacity/wait.py
@@ -189,3 +189,37 @@
     def __call__(self, retry_state: "RetryCallState") -> float:
         high = super().__call__(retry_state=retry_state)
         return random.uniform(0, high)
+
+
+class wait_exponential_jitter(wait_base):
+    """Wait strategy that applies exponential backoff and jitter.
+
+    It allows for a customized initial wait, maximum wait and jitter.
+
+    This implements the strategy described here:
+    https://cloud.google.com/storage/docs/retry-strategy
+
+    The wait time is min(initial * (2**n + random.uniform(0, jitter)), maximum)
+    where n is the retry count.
+    """
+
+    def __init__(
+        self,
+        initial: float = 1,
+        max: float = _utils.MAX_WAIT,  # noqa
+        exp_base: float = 2,
+        jitter: float = 1,
+    ) -> None:
+        self.initial = initial
+        self.max = max
+        self.exp_base = exp_base
+        self.jitter = jitter
+
+    def __call__(self, retry_state: "RetryCallState") -> float:
+        jitter = random.uniform(0, self.jitter)
+        try:
+            exp = self.exp_base ** (retry_state.attempt_number - 1)
+            result = self.initial * exp + jitter
+        except OverflowError:
+            result = self.max
+        return max(0, min(result, self.max))
diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py
index b9016e7..d9a4858 100644
--- a/tests/test_tenacity.py
+++ b/tests/test_tenacity.py
@@ -435,6 +435,28 @@
         self._assert_inclusive_epsilon(mean(attempt[8]), 30, 2.56)
         self._assert_inclusive_epsilon(mean(attempt[9]), 30, 2.56)
 
+    def test_wait_exponential_jitter(self):
+        fn = tenacity.wait_exponential_jitter(max=60)
+
+        for _ in range(1000):
+            self._assert_inclusive_range(fn(make_retry_state(1, 0)), 1, 2)
+            self._assert_inclusive_range(fn(make_retry_state(2, 0)), 2, 3)
+            self._assert_inclusive_range(fn(make_retry_state(3, 0)), 4, 5)
+            self._assert_inclusive_range(fn(make_retry_state(4, 0)), 8, 9)
+            self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17)
+            self._assert_inclusive_range(fn(make_retry_state(6, 0)), 32, 33)
+            self.assertEqual(fn(make_retry_state(7, 0)), 60)
+            self.assertEqual(fn(make_retry_state(8, 0)), 60)
+            self.assertEqual(fn(make_retry_state(9, 0)), 60)
+
+        fn = tenacity.wait_exponential_jitter(10, 5)
+        for _ in range(1000):
+            self.assertEqual(fn(make_retry_state(1, 0)), 5)
+
+        # Default arguments exist
+        fn = tenacity.wait_exponential_jitter()
+        fn(make_retry_state(0, 0))
+
     def test_wait_retry_state_attributes(self):
         class ExtractCallState(Exception):
             pass