Repository: auth0/auth0-python
Pull request #285 (2021-08-16 22:11:45)

[SDK-2687] Implement automatic rate-limit handling
### Changes

This PR enables support for automatic retry of API requests that encounter a 429 rate-limit status response from the Auth0 API. It will retry, by default, up to 3 times, with a hard-coded cap of 10. It exponentially increases the delay between each request ("exponential backoff"), and uses a jitter technique to slightly randomize the variance of this delay.

- Adds a `RestClientOptions` object to carry a configuration throughout the Management calls of the SDK, from `Auth0` instantiation down through to the `RestClient` calls. There was no existing way of gracefully applying `RestClient` options (`telemetry`, `timeouts`, and now `retries`) without having to write custom instantiation code. This can now all be done just from the `Auth0` constructor.

Note: `RestClientOptions` is implemented in such a way as to provide this new cleaner configuration model without introducing breaking changes. All existing configuration arguments are still supported. If a `RestClientOptions` is not provided to `RestClient` during instantiation, one is created dynamically using any provided timeout or telemetry options passed as arguments. We should deprecate using the argument configurations in the future and transition developers toward using `RestClientOptions` directly.

- Updates `Auth0` constructor to accept `rest_options` argument for a `RestClientOptions` configuration object. This is further passed on to the constructors for all the Management endpoint classes.
- Updates `Blacklists`, `ClientGrants`, `Clients`, `Connections`, `CustomDomains`, `DeviceCredentials`, `EmailTemplates`, `Emails`, `Grants`, `Guardian`, `Hooks`, `Jobs`, `LogStreams`, `Logs`, `Organizations`, `ResourceServers`, `Roles`, `RulesConfigs`, `Rules`, `Stats`, `Tenants`, `Tickets`, `UserBlocks`, `UsersByEmail`, and `Users` constructors to accept `rest_options` argument for a `RestClientOptions` configuration object. 
- Updates `RestClient` constructor to accept `options` argument for a `RestClientOptions` configuration object.
- Updates `RestClient.get()` with automatic retry logic.
- Adds `RestClient.MAX_REQUEST_RETRIES()`, `MAX_REQUEST_RETRY_JITTER()`, `MAX_REQUEST_RETRY_DELAY()` and `MIN_REQUEST_RETRY_DELAY()` getters to read the hard limits from, to avoid using mutable values.
- Adds `RestClient._metrics` to aid in unit testing this new logic. This dict keeps track of the number of retry attempts, and the individual backoff windows applied to each retry.
- Adds `RestClient._skip_sleep` to avoid actually calling sleep() during unit tests and greatly slowing down the test process.
- Updates `test_get_rate_limit_error` and `test_get_rate_limit_error_without_headers`, and adds `test_get_rate_limit_custom_retries`, `test_get_rate_limit_invalid_retries_below_min`, `test_get_rate_limit_invalid_retries_above_max` and `test_get_rate_limit_retries_use_exponential_backoff` to unit tests to cover new functionality.
- Updates other tests to prefer reading from the new internal configuration model (although technically they would work without changes until the old model is deprecated.)

### References

Please see the internal SDK-2653 epic on this topic.

### Testing

Tests can be executed using `python3 -m unittest -v`.

- [x] This change adds unit test coverage
- [ ] This change adds integration test coverage
- [x] This change has been tested on the latest version of the platform/language or why not

### Checklist

- [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [x] All existing and new tests complete without errors



Patch:
diff --git a/auth0/v3/management/auth0.py b/auth0/v3/management/auth0.py
index f6b3686..9ac0be9 100644
--- a/auth0/v3/management/auth0.py
+++ b/auth0/v3/management/auth0.py
@@ -32,31 +32,36 @@ class Auth0(object):
         domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
 
         token (str): Management API v2 Token
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token):
-        self.blacklists = Blacklists(domain, token)
-        self.client_grants = ClientGrants(domain, token)
-        self.clients = Clients(domain, token)
-        self.connections = Connections(domain, token)
-        self.custom_domains = CustomDomains(domain, token)
-        self.device_credentials = DeviceCredentials(domain, token)
-        self.email_templates = EmailTemplates(domain, token)
-        self.emails = Emails(domain, token)
-        self.grants = Grants(domain, token)
-        self.guardian = Guardian(domain, token)
-        self.hooks = Hooks(domain, token)
-        self.jobs = Jobs(domain, token)
-        self.log_streams = LogStreams(domain, token)
-        self.logs = Logs(domain, token)
-        self.organizations = Organizations(domain, token)
-        self.resource_servers = ResourceServers(domain, token)
-        self.roles = Roles(domain, token)
-        self.rules_configs = RulesConfigs(domain, token)
-        self.rules = Rules(domain, token)
-        self.stats = Stats(domain, token)
-        self.tenants = Tenants(domain, token)
-        self.tickets = Tickets(domain, token)
-        self.user_blocks = UserBlocks(domain, token)
-        self.users_by_email = UsersByEmail(domain, token)
-        self.users = Users(domain, token)
+    def __init__(self, domain, token, rest_options=None):
+        self.blacklists = Blacklists(domain=domain, token=token, rest_options=rest_options)
+        self.client_grants = ClientGrants(domain=domain, token=token, rest_options=rest_options)
+        self.clients = Clients(domain=domain, token=token, rest_options=rest_options)
+        self.connections = Connections(domain=domain, token=token, rest_options=rest_options)
+        self.custom_domains = CustomDomains(domain=domain, token=token, rest_options=rest_options)
+        self.device_credentials = DeviceCredentials(domain=domain, token=token, rest_options=rest_options)
+        self.email_templates = EmailTemplates(domain=domain, token=token, rest_options=rest_options)
+        self.emails = Emails(domain=domain, token=token, rest_options=rest_options)
+        self.grants = Grants(domain=domain, token=token, rest_options=rest_options)
+        self.guardian = Guardian(domain=domain, token=token, rest_options=rest_options)
+        self.hooks = Hooks(domain=domain, token=token, rest_options=rest_options)
+        self.jobs = Jobs(domain=domain, token=token, rest_options=rest_options)
+        self.log_streams = LogStreams(domain=domain, token=token, rest_options=rest_options)
+        self.logs = Logs(domain=domain, token=token, rest_options=rest_options)
+        self.organizations = Organizations(domain=domain, token=token, rest_options=rest_options)
+        self.resource_servers = ResourceServers(domain=domain, token=token, rest_options=rest_options)
+        self.roles = Roles(domain=domain, token=token, rest_options=rest_options)
+        self.rules_configs = RulesConfigs(domain=domain, token=token, rest_options=rest_options)
+        self.rules = Rules(domain=domain, token=token, rest_options=rest_options)
+        self.stats = Stats(domain=domain, token=token, rest_options=rest_options)
+        self.tenants = Tenants(domain=domain, token=token, rest_options=rest_options)
+        self.tickets = Tickets(domain=domain, token=token, rest_options=rest_options)
+        self.user_blocks = UserBlocks(domain=domain, token=token, rest_options=rest_options)
+        self.users_by_email = UsersByEmail(domain=domain, token=token, rest_options=rest_options)
+        self.users = Users(domain=domain, token=token, rest_options=rest_options)
diff --git a/auth0/v3/management/blacklists.py b/auth0/v3/management/blacklists.py
index 5ef2357..d090649 100644
--- a/auth0/v3/management/blacklists.py
+++ b/auth0/v3/management/blacklists.py
@@ -16,11 +16,16 @@ class Blacklists(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.url = '{}://{}/api/v2/blacklists/tokens'.format(protocol, domain)
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def get(self, aud=None):
         """Retrieves the jti and aud of all tokens in the blacklist.
diff --git a/auth0/v3/management/client_grants.py b/auth0/v3/management/client_grants.py
index 5b11b61..86499a5 100644
--- a/auth0/v3/management/client_grants.py
+++ b/auth0/v3/management/client_grants.py
@@ -16,12 +16,17 @@ class ClientGrants(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/client-grants'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/clients.py b/auth0/v3/management/clients.py
index b098f2a..c1ba9a1 100644
--- a/auth0/v3/management/clients.py
+++ b/auth0/v3/management/clients.py
@@ -16,12 +16,17 @@ class Clients(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/clients'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/connections.py b/auth0/v3/management/connections.py
index 1172657..40c9367 100644
--- a/auth0/v3/management/connections.py
+++ b/auth0/v3/management/connections.py
@@ -16,12 +16,17 @@ class Connections(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/connections'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/custom_domains.py b/auth0/v3/management/custom_domains.py
index d7e6c0b..f681d62 100644
--- a/auth0/v3/management/custom_domains.py
+++ b/auth0/v3/management/custom_domains.py
@@ -16,12 +16,17 @@ class CustomDomains(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/custom-domains'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/device_credentials.py b/auth0/v3/management/device_credentials.py
index 0667288..fda6c81 100644
--- a/auth0/v3/management/device_credentials.py
+++ b/auth0/v3/management/device_credentials.py
@@ -16,12 +16,17 @@ class DeviceCredentials(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/device-credentials'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/email_templates.py b/auth0/v3/management/email_templates.py
index 7f97c78..6091b1c 100644
--- a/auth0/v3/management/email_templates.py
+++ b/auth0/v3/management/email_templates.py
@@ -16,12 +16,17 @@ class EmailTemplates(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/email-templates'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/emails.py b/auth0/v3/management/emails.py
index ea15738..34813ea 100644
--- a/auth0/v3/management/emails.py
+++ b/auth0/v3/management/emails.py
@@ -16,12 +16,17 @@ class Emails(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/emails/provider'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/grants.py b/auth0/v3/management/grants.py
index 37cd336..24af4a6 100644
--- a/auth0/v3/management/grants.py
+++ b/auth0/v3/management/grants.py
@@ -16,12 +16,17 @@ class Grants(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/grants'.format(self.protocol, self.domain)
@@ -45,7 +50,7 @@ class Grants(object):
            extra_params (dictionary, optional): The extra parameters to add to
                the request. The page, per_page, and include_totals values
                specified as parameters take precedence over the ones defined here.
-            
+
         See: https://auth0.com/docs/api/management/v2#!/Grants/get_grants
         """
         params = extra_params or {}
diff --git a/auth0/v3/management/guardian.py b/auth0/v3/management/guardian.py
index 0e96669..a65f2a6 100644
--- a/auth0/v3/management/guardian.py
+++ b/auth0/v3/management/guardian.py
@@ -16,12 +16,17 @@ class Guardian(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/guardian'.format(self.protocol, self.domain)
@@ -33,7 +38,7 @@ class Guardian(object):
         """Retrieves all factors. Useful to check factor enablement and
              trial status.
 
-        See: https://auth0.com/docs/api/management/v2#!/Guardian/get_factors                 
+        See: https://auth0.com/docs/api/management/v2#!/Guardian/get_factors
         """
 
         return self.client.get(self._url('factors'))
diff --git a/auth0/v3/management/hooks.py b/auth0/v3/management/hooks.py
index d3c4e9b..2d8c4c0 100644
--- a/auth0/v3/management/hooks.py
+++ b/auth0/v3/management/hooks.py
@@ -17,12 +17,17 @@ class Hooks(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = "{}://{}/api/v2/hooks".format(self.protocol, self.domain)
diff --git a/auth0/v3/management/jobs.py b/auth0/v3/management/jobs.py
index 0bf2220..bc4d5b4 100644
--- a/auth0/v3/management/jobs.py
+++ b/auth0/v3/management/jobs.py
@@ -17,12 +17,17 @@ class Jobs(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, path=None):
         url = '{}://{}/api/v2/jobs'.format(self.protocol, self.domain)
@@ -57,10 +62,10 @@ class Jobs(object):
         Args:
             job_id (str): The id of the job.
 
-        Deprecation: 
+        Deprecation:
             The /jobs/{id}/results endpoint was removed from the Management API.
             You can obtain the Job results by querying a Job by ID.
-            
+
         See: https://auth0.com/docs/api/management/v2#!/Jobs/get_jobs_by_id
         """
         warnings.warn("/jobs/{id}/results is no longer available. The get(id) function will be called instead.", DeprecationWarning)
diff --git a/auth0/v3/management/log_streams.py b/auth0/v3/management/log_streams.py
index 16f4e95..86e9dd7 100644
--- a/auth0/v3/management/log_streams.py
+++ b/auth0/v3/management/log_streams.py
@@ -16,12 +16,17 @@ class LogStreams(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/log-streams'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/logs.py b/auth0/v3/management/logs.py
index 81ceb9f..aa8b646 100644
--- a/auth0/v3/management/logs.py
+++ b/auth0/v3/management/logs.py
@@ -16,12 +16,17 @@ class Logs(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/logs'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/organizations.py b/auth0/v3/management/organizations.py
index 21df879..653a7e3 100644
--- a/auth0/v3/management/organizations.py
+++ b/auth0/v3/management/organizations.py
@@ -16,12 +16,17 @@ class Organizations(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, *args):
         url = '{}://{}/api/v2/organizations'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/resource_servers.py b/auth0/v3/management/resource_servers.py
index ce89136..61ca4e9 100644
--- a/auth0/v3/management/resource_servers.py
+++ b/auth0/v3/management/resource_servers.py
@@ -16,12 +16,17 @@ class ResourceServers(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/resource-servers'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/rest.py b/auth0/v3/management/rest.py
index e8dc1fa..e8a537e 100644
--- a/auth0/v3/management/rest.py
+++ b/auth0/v3/management/rest.py
@@ -2,13 +2,46 @@ import base64
 import json
 import platform
 import sys
-
 import requests
 
 from ..exceptions import Auth0Error, RateLimitError
+from time import sleep
+from random import randint
 
 UNKNOWN_ERROR = 'a0.sdk.internal.unknown'
 
+class RestClientOptions(object):
+    """Configuration object for RestClient. Used for configuring
+            additional RestClient options, such as rate-limit
+            retries.
+
+    Args:
+        telemetry (bool, optional): Enable or disable Telemetry
+            (defaults to True)
+        timeout (float or tuple, optional): Change the requests
+            connect and read timeout. Pass a tuple to specify
+            both values separately or a float to set both to it.
+            (defaults to 5.0 for both)
+        retries (integer): In the event an API request returns a
+            429 response header (indicating rate-limit has been
+            hit), the RestClient will retry the request this many
+            times using an exponential backoff strategy, before
+            raising a RateLimitError exception. 10 retries max.
+            (defaults to 3)
+    """
+    def __init__(self, telemetry=None, timeout=None, retries=None):
+        self.telemetry = True
+        self.timeout = 5.0
+        self.retries = 3
+
+        if telemetry is not None:
+            self.telemetry = telemetry
+
+        if timeout is not None:
+            self.timeout = timeout
+
+        if retries is not None:
+            self.retries = retries
 
 class RestClient(object):
     """Provides simple methods for handling all RESTful api endpoints.
@@ -20,17 +53,29 @@ class RestClient(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+        options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries. Overrides matching
+            options passed to the constructor.
+            (defaults to 3)
     """
 
-    def __init__(self, jwt, telemetry=True, timeout=5.0):
+    def __init__(self, jwt, telemetry=True, timeout=5.0, options=None):
+        if options is None:
+            options = RestClientOptions(telemetry=telemetry, timeout=timeout)
+
+        self.options = options
         self.jwt = jwt
-        self.timeout = timeout
+
+        self._metrics = {'retries': 0, 'backoff': []}
+        self._skip_sleep = False
 
         self.base_headers = {
             'Authorization': 'Bearer {}'.format(self.jwt),
             'Content-Type': 'application/json',
         }
-        if telemetry:
+
+        if options.telemetry:
             py_version = platform.python_version()
             version = sys.modules['auth0'].__version__
 
@@ -47,41 +92,105 @@ class RestClient(object):
                 'Auth0-Client': base64.b64encode(auth0_client),
             })
 
+        # For backwards compatibility reasons only
+        # TODO: Deprecate in the next major so we can prune these arguments. Guidance should be to use RestClient.options.*
+        self.telemetry = options.telemetry
+        self.timeout = options.timeout
+
+    # Returns a hard cap for the maximum number of retries allowed (10)
+    def MAX_REQUEST_RETRIES(self):
+        return 10
+
+    # Returns the maximum amount of jitter to introduce in milliseconds (100ms)
+    def MAX_REQUEST_RETRY_JITTER(self):
+        return 100
+
+    # Returns the maximum delay window allowed (1000ms)
+    def MAX_REQUEST_RETRY_DELAY(self):
+        return 1000
+
+    # Returns the minimum delay window allowed (100ms)
+    def MIN_REQUEST_RETRY_DELAY(self):
+        return 100
+
     def get(self, url, params=None):
         headers = self.base_headers.copy()
 
-        response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
+        # Track the API request attempt number
+        attempt = 0
+
+        # Reset the metrics tracker
+        self._metrics = {'retries': 0, 'backoff': []}
+
+        # Cap the maximum number of retries to 10 or fewer. Floor the retries at 0.
+        retries = min(self.MAX_REQUEST_RETRIES(), max(0, self.options.retries))
+
+        while True:
+            # Increment attempt number
+            attempt += 1
+
+            # Issue the request
+            response = requests.get(url, params=params, headers=headers, timeout=self.options.timeout);
+
+            # If the response did not have a 429 header, or the retries were configured at 0, or the attempt number is equal to or greater than the configured retries, break
+            if response.status_code != 429 or retries <= 0 or attempt > retries:
+                break
+
+            # Retry the request. Apply a exponential backoff for subsequent attempts, using this formula:
+            # max(MIN_REQUEST_RETRY_DELAY, min(MAX_REQUEST_RETRY_DELAY, (100ms * (2 ** attempt - 1)) + random_between(1, MAX_REQUEST_RETRY_JITTER)))
+
+            # Increases base delay by (100ms * (2 ** attempt - 1))
+            wait = 100 * 2 ** (attempt - 1)
+
+            # Introduces jitter to the base delay; increases delay between 1ms to MAX_REQUEST_RETRY_JITTER (100ms)
+            wait += randint(1, self.MAX_REQUEST_RETRY_JITTER())
+
+            # Is never more than MAX_REQUEST_RETRY_DELAY (1s)
+            wait = min(self.MAX_REQUEST_RETRY_DELAY(), wait)
+
+            # Is never less than MIN_REQUEST_RETRY_DELAY (100ms)
+            wait = max(self.MIN_REQUEST_RETRY_DELAY(), wait)
+
+            self._metrics['retries'] = attempt
+            self._metrics['backoff'].append(wait)
+
+            # Skip calling sleep() when running unit tests
+            if self._skip_sleep is False:
+                # sleep() functions in seconds, so convert the milliseconds formula above accordingly
+                sleep(wait / 1000)
+
+        # Return the final Response
         return self._process_response(response)
 
     def post(self, url, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.post(url, json=data, headers=headers, timeout=self.timeout)
+        response = requests.post(url, json=data, headers=headers, timeout=self.options.timeout)
         return self._process_response(response)
 
     def file_post(self, url, data=None, files=None):
         headers = self.base_headers.copy()
         headers.pop('Content-Type', None)
 
-        response = requests.post(url, data=data, files=files, headers=headers, timeout=self.timeout)
+        response = requests.post(url, data=data, files=files, headers=headers, timeout=self.options.timeout)
         return self._process_response(response)
 
     def patch(self, url, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.patch(url, json=data, headers=headers, timeout=self.timeout)
+        response = requests.patch(url, json=data, headers=headers, timeout=self.options.timeout)
         return self._process_response(response)
 
     def put(self, url, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.put(url, json=data, headers=headers, timeout=self.timeout)
+        response = requests.put(url, json=data, headers=headers, timeout=self.options.timeout)
         return self._process_response(response)
 
     def delete(self, url, params=None, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.delete(url, headers=headers, params=params or {}, json=data, timeout=self.timeout)
+        response = requests.delete(url, headers=headers, params=params or {}, json=data, timeout=self.options.timeout)
         return self._process_response(response)
 
     def _process_response(self, response):
diff --git a/auth0/v3/management/roles.py b/auth0/v3/management/roles.py
index 140a053..700e0d7 100644
--- a/auth0/v3/management/roles.py
+++ b/auth0/v3/management/roles.py
@@ -16,12 +16,17 @@ class Roles(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/roles'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/rules.py b/auth0/v3/management/rules.py
index 2cd7627..cf70c87 100644
--- a/auth0/v3/management/rules.py
+++ b/auth0/v3/management/rules.py
@@ -16,12 +16,17 @@ class Rules(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/rules'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/rules_configs.py b/auth0/v3/management/rules_configs.py
index dc4f8a8..04ddb65 100644
--- a/auth0/v3/management/rules_configs.py
+++ b/auth0/v3/management/rules_configs.py
@@ -16,12 +16,17 @@ class RulesConfigs(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/rules-configs'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/stats.py b/auth0/v3/management/stats.py
index a860330..a30d361 100644
--- a/auth0/v3/management/stats.py
+++ b/auth0/v3/management/stats.py
@@ -16,12 +16,17 @@ class Stats(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, action):
         return '{}://{}/api/v2/stats/{}'.format(self.protocol, self.domain, action)
diff --git a/auth0/v3/management/tenants.py b/auth0/v3/management/tenants.py
index 09a51f8..958c723 100644
--- a/auth0/v3/management/tenants.py
+++ b/auth0/v3/management/tenants.py
@@ -16,12 +16,17 @@ class Tenants(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self):
         return '{}://{}/api/v2/tenants/settings'.format(self.protocol, self.domain)
@@ -36,7 +41,7 @@ class Tenants(object):
 
            include_fields (bool, optional): True if the fields specified are
               to be included in the result, False otherwise. Defaults to True.
-              
+
         See: https://auth0.com/docs/api/management/v2#!/Tenants/get_settings
         """
 
diff --git a/auth0/v3/management/tickets.py b/auth0/v3/management/tickets.py
index a5e1bfc..004a951 100644
--- a/auth0/v3/management/tickets.py
+++ b/auth0/v3/management/tickets.py
@@ -16,12 +16,17 @@ class Tickets(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, action):
         return '{}://{}/api/v2/tickets/{}'.format(self.protocol, self.domain, action)
diff --git a/auth0/v3/management/user_blocks.py b/auth0/v3/management/user_blocks.py
index 402e7dd..7df1c82 100644
--- a/auth0/v3/management/user_blocks.py
+++ b/auth0/v3/management/user_blocks.py
@@ -16,12 +16,17 @@ class UserBlocks(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/user-blocks'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/users.py b/auth0/v3/management/users.py
index b4c20e7..3686d08 100644
--- a/auth0/v3/management/users.py
+++ b/auth0/v3/management/users.py
@@ -18,12 +18,17 @@ class Users(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self, id=None):
         url = '{}://{}/api/v2/users'.format(self.protocol, self.domain)
diff --git a/auth0/v3/management/users_by_email.py b/auth0/v3/management/users_by_email.py
index 25d0227..64f00c3 100644
--- a/auth0/v3/management/users_by_email.py
+++ b/auth0/v3/management/users_by_email.py
@@ -16,12 +16,17 @@ class UsersByEmail(object):
             connect and read timeout. Pass a tuple to specify
             both values separately or a float to set both to it.
             (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
     """
 
-    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https"):
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
         self.domain = domain
         self.protocol = protocol
-        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout)
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
 
     def _url(self):
         url = '{}://{}/api/v2/users-by-email'.format(self.protocol, self.domain)


Test patch:
diff --git a/auth0/v3/test/management/test_blacklists.py b/auth0/v3/test/management/test_blacklists.py
index 7c62e57..5176308 100644
--- a/auth0/v3/test/management/test_blacklists.py
+++ b/auth0/v3/test/management/test_blacklists.py
@@ -7,7 +7,7 @@ class TestBlacklists(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Blacklists(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_client_grants.py b/auth0/v3/test/management/test_client_grants.py
index 399c5cb..d66fe15 100644
--- a/auth0/v3/test/management/test_client_grants.py
+++ b/auth0/v3/test/management/test_client_grants.py
@@ -7,7 +7,7 @@ class TestClientGrants(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = ClientGrants(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_clients.py b/auth0/v3/test/management/test_clients.py
index 58be544..b089fd6 100644
--- a/auth0/v3/test/management/test_clients.py
+++ b/auth0/v3/test/management/test_clients.py
@@ -7,7 +7,7 @@ class TestClients(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Clients(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_connections.py b/auth0/v3/test/management/test_connections.py
index 841f7e8..e89d104 100644
--- a/auth0/v3/test/management/test_connections.py
+++ b/auth0/v3/test/management/test_connections.py
@@ -7,7 +7,7 @@ class TestConnection(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Connections(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_custom_domains.py b/auth0/v3/test/management/test_custom_domains.py
index 2a42dbb..d74e1b6 100644
--- a/auth0/v3/test/management/test_custom_domains.py
+++ b/auth0/v3/test/management/test_custom_domains.py
@@ -7,7 +7,7 @@ class TestCustomDomains(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = CustomDomains(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_device_credentials.py b/auth0/v3/test/management/test_device_credentials.py
index c720ba6..6aa854d 100644
--- a/auth0/v3/test/management/test_device_credentials.py
+++ b/auth0/v3/test/management/test_device_credentials.py
@@ -7,7 +7,7 @@ class TestDeviceCredentials(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = DeviceCredentials(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_email_endpoints.py b/auth0/v3/test/management/test_email_endpoints.py
index 5ebd190..c0aea86 100644
--- a/auth0/v3/test/management/test_email_endpoints.py
+++ b/auth0/v3/test/management/test_email_endpoints.py
@@ -7,7 +7,7 @@ class TestClients(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = EmailTemplates(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_emails.py b/auth0/v3/test/management/test_emails.py
index e212c67..43222f8 100644
--- a/auth0/v3/test/management/test_emails.py
+++ b/auth0/v3/test/management/test_emails.py
@@ -7,7 +7,7 @@ class TestEmails(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Emails(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_grants.py b/auth0/v3/test/management/test_grants.py
index a9325c0..f818fe9 100644
--- a/auth0/v3/test/management/test_grants.py
+++ b/auth0/v3/test/management/test_grants.py
@@ -7,7 +7,7 @@ class TestGrants(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Grants(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_guardian.py b/auth0/v3/test/management/test_guardian.py
index 3c7b299..c3656ad 100644
--- a/auth0/v3/test/management/test_guardian.py
+++ b/auth0/v3/test/management/test_guardian.py
@@ -7,7 +7,7 @@ class TestGuardian(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Guardian(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_hooks.py b/auth0/v3/test/management/test_hooks.py
index 5376b1c..9137eba 100644
--- a/auth0/v3/test/management/test_hooks.py
+++ b/auth0/v3/test/management/test_hooks.py
@@ -7,7 +7,7 @@ class TestRules(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Hooks(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_jobs.py b/auth0/v3/test/management/test_jobs.py
index 203185e..1a3ba26 100644
--- a/auth0/v3/test/management/test_jobs.py
+++ b/auth0/v3/test/management/test_jobs.py
@@ -7,7 +7,7 @@ class TestJobs(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Jobs(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_log_streams.py b/auth0/v3/test/management/test_log_streams.py
index 94f3d36..42f1d00 100644
--- a/auth0/v3/test/management/test_log_streams.py
+++ b/auth0/v3/test/management/test_log_streams.py
@@ -9,7 +9,7 @@ class TestLogStreams(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = LogStreams(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
@@ -24,7 +24,7 @@ class TestLogStreams(unittest.TestCase):
         args, kwargs = mock_instance.get.call_args
 
         self.assertEqual('https://domain/api/v2/log-streams', args[0])
-        
+
     @mock.patch('auth0.v3.management.log_streams.RestClient')
     def test_get(self, mock_rc):
         mock_instance = mock_rc.return_value
diff --git a/auth0/v3/test/management/test_logs.py b/auth0/v3/test/management/test_logs.py
index 3075921..84acdf4 100644
--- a/auth0/v3/test/management/test_logs.py
+++ b/auth0/v3/test/management/test_logs.py
@@ -7,7 +7,7 @@ class TestLogs(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Logs(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_organizations.py b/auth0/v3/test/management/test_organizations.py
index 69ce681..192016e 100644
--- a/auth0/v3/test/management/test_organizations.py
+++ b/auth0/v3/test/management/test_organizations.py
@@ -7,7 +7,7 @@ class TestOrganizations(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Organizations(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_resource_servers.py b/auth0/v3/test/management/test_resource_servers.py
index 340bfc4..911bf23 100644
--- a/auth0/v3/test/management/test_resource_servers.py
+++ b/auth0/v3/test/management/test_resource_servers.py
@@ -7,7 +7,7 @@ class TestResourceServers(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = ResourceServers(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_rest.py b/auth0/v3/test/management/test_rest.py
index 05a88ac..fb38ee3 100644
--- a/auth0/v3/test/management/test_rest.py
+++ b/auth0/v3/test/management/test_rest.py
@@ -6,11 +6,95 @@ import base64
 import mock
 import requests
 
-from ...management.rest import RestClient
+from ...management.rest import RestClient, RestClientOptions
 from ...exceptions import Auth0Error, RateLimitError
 
 
 class TestRest(unittest.TestCase):
+    def test_options_are_used_and_override(self):
+        """
+        This test ensures RestClientOptions are read when passed to
+        RestClient's constructor by (1) configuring a timeout and (2)
+        turning off Telemetry. This proves that RestClient is inheriting
+        those options, and overriding it's own constructor arguments.
+        """
+
+        options = RestClientOptions(telemetry=False, timeout=0.00001, retries=10)
+        rc = RestClient(jwt='a-token', telemetry=True, timeout=30, options=options)
+
+        # Does a timeout occur as expected?
+        with self.assertRaises(requests.exceptions.Timeout):
+            rc.get('http://google.com')
+
+        # Is RestClient using the RestClientOptions.timeout value properly?
+        self.assertEqual(rc.options.timeout, 0.00001)
+
+        # Is RestClient using the RestClientOptions.retries value properly?
+        self.assertEqual(rc.options.retries, 10)
+
+        # Is RestClient using the RestClientOptions.telemetry value properly?
+        self.assertEqual(rc.options.telemetry, False)
+
+        # Is RestClient using the RestClientOptions.telemetry value properly?
+        self.assertEqual(rc.base_headers, {
+            'Content-Type': 'application/json',
+            'Authorization': 'Bearer a-token',
+        })
+
+    def test_options_are_created_by_default(self):
+        """
+        This test ensures RestClientOptions are read when passed to
+        RestClient's constructor by (1) configuring a timeout and (2)
+        turning off Telemetry. This proves that RestClient is inheriting
+        those options, and overriding it's own constructor arguments.
+        """
+
+        rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00002)
+
+        # Does a timeout occur as expected?
+        with self.assertRaises(requests.exceptions.Timeout):
+            rc.get('http://google.com')
+
+        # Did RestClient create a RestClientOptions for us?
+        self.assertIsNotNone(rc.options)
+
+        # Did RestClient assign the new RestClientOptions instance the proper timeout value from the constructor?
+        self.assertEqual(rc.options.timeout, 0.00002)
+
+        # Did RestClient use the default RestClientOptions value for retries?
+        self.assertEqual(rc.options.retries, 3)
+
+        # Did RestClient assign the new RestClientOptions instance the proper telemetry value from the constructor?
+        self.assertEqual(rc.options.telemetry, False)
+
+        # Is RestClient using the RestClientOptions.telemetry value properly?
+        self.assertEqual(rc.base_headers, {
+            'Content-Type': 'application/json',
+            'Authorization': 'Bearer a-token',
+        })
+
+    def test_default_options_are_used(self):
+        """
+        This test ensures RestClientOptions are read when passed to
+        RestClient's constructor by (1) configuring a timeout and (2)
+        turning off Telemetry. This proves that RestClient is inheriting
+        those options, and overriding it's own constructor arguments.
+        """
+
+        options = RestClientOptions()
+        rc = RestClient(jwt='a-token', options=options)
+
+        # Did RestClient store the RestClientOptions?
+        self.assertIsNotNone(rc.options)
+
+        # Did RestClientOptions use the default 5.0 timeout?
+        self.assertEqual(rc.options.timeout, 5.0)
+
+        # Did RestClientOptions use the default 3 retries?
+        self.assertEqual(rc.options.retries, 3)
+
+        # Did RestClientOptions use the default True telemetry value?
+        self.assertEqual(rc.options.telemetry, True)
 
     def test_get_can_timeout(self):
         rc = RestClient(jwt='a-token', telemetry=False, timeout=0.00001)
@@ -151,7 +235,9 @@ class TestRest(unittest.TestCase):
 
     @mock.patch('requests.get')
     def test_get_rate_limit_error(self, mock_get):
-        rc = RestClient(jwt='a-token', telemetry=False)
+        options = RestClientOptions(telemetry=False, retries=0)
+        rc = RestClient(jwt='a-token', options=options)
+        rc._skip_sleep = True
 
         mock_get.return_value.text = '{"statusCode": 429,' \
                                      ' "errorCode": "code",' \
@@ -172,15 +258,18 @@ class TestRest(unittest.TestCase):
         self.assertIsInstance(context.exception, RateLimitError)
         self.assertEqual(context.exception.reset_at, 9)
 
+        self.assertEqual(rc._metrics['retries'], 0)
+
     @mock.patch('requests.get')
     def test_get_rate_limit_error_without_headers(self, mock_get):
-        rc = RestClient(jwt='a-token', telemetry=False)
+        options = RestClientOptions(telemetry=False, retries=1)
+        rc = RestClient(jwt='a-token', options=options)
 
         mock_get.return_value.text = '{"statusCode": 429,' \
                                      ' "errorCode": "code",' \
                                      ' "message": "message"}'
         mock_get.return_value.status_code = 429
-        
+
         mock_get.return_value.headers = {}
         with self.assertRaises(Auth0Error) as context:
             rc.get('the/url')
@@ -191,6 +280,179 @@ class TestRest(unittest.TestCase):
         self.assertIsInstance(context.exception, RateLimitError)
         self.assertEqual(context.exception.reset_at, -1)
 
+        self.assertEqual(rc._metrics['retries'], 1)
+
+    @mock.patch('requests.get')
+    def test_get_rate_limit_custom_retries(self, mock_get):
+        options = RestClientOptions(telemetry=False, retries=5)
+        rc = RestClient(jwt='a-token', options=options)
+        rc._skip_sleep = True
+
+        mock_get.return_value.text = '{"statusCode": 429,' \
+                                     ' "errorCode": "code",' \
+                                     ' "message": "message"}'
+        mock_get.return_value.status_code = 429
+        mock_get.return_value.headers = {
+            'x-ratelimit-limit': '3',
+            'x-ratelimit-remaining': '6',
+            'x-ratelimit-reset': '9',
+        }
+
+        with self.assertRaises(Auth0Error) as context:
+            response = rc.get('the/url')
+
+        self.assertEqual(context.exception.status_code, 429)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+        self.assertIsInstance(context.exception, RateLimitError)
+        self.assertEqual(context.exception.reset_at, 9)
+
+        self.assertEqual(rc._metrics['retries'], 5)
+        self.assertEqual(rc._metrics['retries'], len(rc._metrics['backoff']))
+
+    @mock.patch('requests.get')
+    def test_get_rate_limit_invalid_retries_below_min(self, mock_get):
+        options = RestClientOptions(telemetry=False, retries=-1)
+        rc = RestClient(jwt='a-token', options=options)
+        rc._skip_sleep = True
+
+        mock_get.return_value.text = '{"statusCode": 429,' \
+                                     ' "errorCode": "code",' \
+                                     ' "message": "message"}'
+        mock_get.return_value.status_code = 429
+        mock_get.return_value.headers = {
+            'x-ratelimit-limit': '3',
+            'x-ratelimit-remaining': '6',
+            'x-ratelimit-reset': '9',
+        }
+
+        with self.assertRaises(Auth0Error) as context:
+            response = rc.get('the/url')
+
+        self.assertEqual(context.exception.status_code, 429)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+        self.assertIsInstance(context.exception, RateLimitError)
+        self.assertEqual(context.exception.reset_at, 9)
+
+        self.assertEqual(rc._metrics['retries'], 0)
+
+
+    @mock.patch('requests.get')
+    def test_get_rate_limit_invalid_retries_above_max(self, mock_get):
+        options = RestClientOptions(telemetry=False, retries=11)
+        rc = RestClient(jwt='a-token', options=options)
+        rc._skip_sleep = True
+
+        mock_get.return_value.text = '{"statusCode": 429,' \
+                                     ' "errorCode": "code",' \
+                                     ' "message": "message"}'
+        mock_get.return_value.status_code = 429
+        mock_get.return_value.headers = {
+            'x-ratelimit-limit': '3',
+            'x-ratelimit-remaining': '6',
+            'x-ratelimit-reset': '9',
+        }
+
+        with self.assertRaises(Auth0Error) as context:
+            response = rc.get('the/url')
+
+        self.assertEqual(context.exception.status_code, 429)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+        self.assertIsInstance(context.exception, RateLimitError)
+        self.assertEqual(context.exception.reset_at, 9)
+
+        self.assertEqual(rc._metrics['retries'], rc.MAX_REQUEST_RETRIES())
+
+    @mock.patch('requests.get')
+    def test_get_rate_limit_retries_use_exponential_backoff(self, mock_get):
+        options = RestClientOptions(telemetry=False, retries=10)
+        rc = RestClient(jwt='a-token', options=options)
+        rc._skip_sleep = True
+
+        mock_get.return_value.text = '{"statusCode": 429,' \
+                                     ' "errorCode": "code",' \
+                                     ' "message": "message"}'
+        mock_get.return_value.status_code = 429
+        mock_get.return_value.headers = {
+            'x-ratelimit-limit': '3',
+            'x-ratelimit-remaining': '6',
+            'x-ratelimit-reset': '9',
+        }
+
+        with self.assertRaises(Auth0Error) as context:
+            response = rc.get('the/url')
+
+        self.assertEqual(context.exception.status_code, 429)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+        self.assertIsInstance(context.exception, RateLimitError)
+        self.assertEqual(context.exception.reset_at, 9)
+
+        self.assertEqual(rc._metrics['retries'], 10)
+        self.assertEqual(rc._metrics['retries'], len(rc._metrics['backoff']))
+
+        baseBackoff = [0]
+        baseBackoffSum = 0
+        finalBackoff = 0
+
+        for i in range(0, 9):
+            backoff = 100 * 2 ** i
+            baseBackoff.append(backoff)
+            baseBackoffSum += backoff
+
+        for backoff in rc._metrics['backoff']:
+            finalBackoff += backoff
+
+        # Assert that exponential backoff is happening.
+        self.assertGreaterEqual(rc._metrics['backoff'][1], rc._metrics['backoff'][0])
+        self.assertGreaterEqual(rc._metrics['backoff'][2], rc._metrics['backoff'][1])
+        self.assertGreaterEqual(rc._metrics['backoff'][3], rc._metrics['backoff'][2])
+        self.assertGreaterEqual(rc._metrics['backoff'][4], rc._metrics['backoff'][3])
+        self.assertGreaterEqual(rc._metrics['backoff'][5], rc._metrics['backoff'][4])
+        self.assertGreaterEqual(rc._metrics['backoff'][6], rc._metrics['backoff'][5])
+        self.assertGreaterEqual(rc._metrics['backoff'][7], rc._metrics['backoff'][6])
+        self.assertGreaterEqual(rc._metrics['backoff'][8], rc._metrics['backoff'][7])
+        self.assertGreaterEqual(rc._metrics['backoff'][9], rc._metrics['backoff'][8])
+
+        # Ensure jitter is being applied.
+        self.assertNotEquals(rc._metrics['backoff'][1], baseBackoff[1])
+        self.assertNotEquals(rc._metrics['backoff'][2], baseBackoff[2])
+        self.assertNotEquals(rc._metrics['backoff'][3], baseBackoff[3])
+        self.assertNotEquals(rc._metrics['backoff'][4], baseBackoff[4])
+        self.assertNotEquals(rc._metrics['backoff'][5], baseBackoff[5])
+        self.assertNotEquals(rc._metrics['backoff'][6], baseBackoff[6])
+        self.assertNotEquals(rc._metrics['backoff'][7], baseBackoff[7])
+        self.assertNotEquals(rc._metrics['backoff'][8], baseBackoff[8])
+        self.assertNotEquals(rc._metrics['backoff'][9], baseBackoff[9])
+
+        # Ensure subsequent delay is never less than the minimum.
+        self.assertGreaterEqual(rc._metrics['backoff'][1], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][2], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][3], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][4], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][5], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][6], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][7], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][8], rc.MIN_REQUEST_RETRY_DELAY())
+        self.assertGreaterEqual(rc._metrics['backoff'][9], rc.MIN_REQUEST_RETRY_DELAY())
+
+        # Ensure delay is never more than the maximum.
+        self.assertLessEqual(rc._metrics['backoff'][0], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][1], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][2], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][3], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][4], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][5], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][6], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][7], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][8], rc.MAX_REQUEST_RETRY_DELAY())
+        self.assertLessEqual(rc._metrics['backoff'][9], rc.MAX_REQUEST_RETRY_DELAY())
+
+        # Ensure total delay sum is never more than 10s.
+        self.assertLessEqual(finalBackoff, 10000)
+
     @mock.patch('requests.post')
     def test_post(self, mock_post):
         rc = RestClient(jwt='a-token', telemetry=False)
diff --git a/auth0/v3/test/management/test_roles.py b/auth0/v3/test/management/test_roles.py
index f2559a3..bd8caff 100644
--- a/auth0/v3/test/management/test_roles.py
+++ b/auth0/v3/test/management/test_roles.py
@@ -7,7 +7,7 @@ class TestRoles(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Roles(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_rules.py b/auth0/v3/test/management/test_rules.py
index 2db7813..d8955da 100644
--- a/auth0/v3/test/management/test_rules.py
+++ b/auth0/v3/test/management/test_rules.py
@@ -7,7 +7,7 @@ class TestRules(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Rules(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_rules_configs.py b/auth0/v3/test/management/test_rules_configs.py
index 9e6babf..f4ee868 100644
--- a/auth0/v3/test/management/test_rules_configs.py
+++ b/auth0/v3/test/management/test_rules_configs.py
@@ -7,7 +7,7 @@ class TestRulesConfigs(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = RulesConfigs(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_stats.py b/auth0/v3/test/management/test_stats.py
index 91f9461..4872759 100644
--- a/auth0/v3/test/management/test_stats.py
+++ b/auth0/v3/test/management/test_stats.py
@@ -7,7 +7,7 @@ class TestStats(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Stats(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_tenants.py b/auth0/v3/test/management/test_tenants.py
index 0838c05..e0c9a86 100644
--- a/auth0/v3/test/management/test_tenants.py
+++ b/auth0/v3/test/management/test_tenants.py
@@ -7,7 +7,7 @@ class TestTenants(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Tenants(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_tickets.py b/auth0/v3/test/management/test_tickets.py
index 3bbd2e5..eafbe62 100644
--- a/auth0/v3/test/management/test_tickets.py
+++ b/auth0/v3/test/management/test_tickets.py
@@ -7,7 +7,7 @@ class TestTickets(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Tickets(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_user_blocks.py b/auth0/v3/test/management/test_user_blocks.py
index c240c04..8d81a8b 100644
--- a/auth0/v3/test/management/test_user_blocks.py
+++ b/auth0/v3/test/management/test_user_blocks.py
@@ -7,7 +7,7 @@ class TestUserBlocks(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = UserBlocks(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_users.py b/auth0/v3/test/management/test_users.py
index 8e03f7e..cbefbbe 100644
--- a/auth0/v3/test/management/test_users.py
+++ b/auth0/v3/test/management/test_users.py
@@ -7,7 +7,7 @@ class TestUsers(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = Users(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)
 
diff --git a/auth0/v3/test/management/test_users_by_email.py b/auth0/v3/test/management/test_users_by_email.py
index 3d947f0..8ab8695 100644
--- a/auth0/v3/test/management/test_users_by_email.py
+++ b/auth0/v3/test/management/test_users_by_email.py
@@ -7,7 +7,7 @@ class TestUsersByEmail(unittest.TestCase):
 
     def test_init_with_optionals(self):
         t = UsersByEmail(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
-        self.assertEqual(t.client.timeout, (10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
         telemetry_header = t.client.base_headers.get('Auth0-Client', None)
         self.assertEqual(telemetry_header, None)